package com.ray3k.skincomposer;

import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.BitmapFontCache;
import com.badlogic.gdx.graphics.g2d.GlyphLayout;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.scenes.scene2d.ui.Label;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Widget;
import com.badlogic.gdx.scenes.scene2d.utils.Drawable;
import com.badlogic.gdx.utils.Align;
import com.badlogic.gdx.utils.Null;
import com.badlogic.gdx.utils.StringBuilder;

public class OpenLabel extends Widget {
    static private final Color tempColor = new Color();
    static private final GlyphLayout prefSizeLayout = new GlyphLayout();
    
    protected Label.LabelStyle style;
    protected final GlyphLayout layout = new GlyphLayout();
    protected final Vector2 prefSize = new Vector2();
    protected final StringBuilder text = new StringBuilder();
    protected int intValue = Integer.MIN_VALUE;
    protected BitmapFontCache cache;
    protected int labelAlign = Align.left;
    protected int lineAlign = Align.left;
    protected boolean wrap;
    protected float lastPrefHeight;
    protected boolean prefSizeInvalid = true;
    protected float fontScaleX = 1, fontScaleY = 1;
    protected boolean fontScaleChanged = false;
    protected @Null String ellipsis;
    
    public OpenLabel (@Null CharSequence text, Skin skin) {
        this(text, skin.get(Label.LabelStyle.class));
    }
    
    public OpenLabel (@Null CharSequence text, Skin skin, String styleName) {
        this(text, skin.get(styleName, Label.LabelStyle.class));
    }
    
    /** Creates a label, using a {@link Label.LabelStyle} that has a BitmapFont with the specified name from the skin and the specified
     * color. */
    public OpenLabel (@Null CharSequence text, Skin skin, String fontName, Color color) {
        this(text, new Label.LabelStyle(skin.getFont(fontName), color));
    }
    
    /** Creates a label, using a {@link Label.LabelStyle} that has a BitmapFont with the specified name and the specified color from the
     * skin. */
    public OpenLabel (@Null CharSequence text, Skin skin, String fontName, String colorName) {
        this(text, new Label.LabelStyle(skin.getFont(fontName), skin.getColor(colorName)));
    }
    
    public OpenLabel (@Null CharSequence text, Label.LabelStyle style) {
        if (text != null) this.text.append(text);
        setStyle(style);
        if (text != null && text.length() > 0) setSize(getPrefWidth(), getPrefHeight());
    }
    
    public void setStyle (Label.LabelStyle style) {
        if (style == null) throw new IllegalArgumentException("style cannot be null.");
        if (style.font == null) throw new IllegalArgumentException("Missing LabelStyle font.");
        this.style = style;
        cache = style.font.newFontCache();
        invalidateHierarchy();
    }
    
    /** Returns the label's style. Modifying the returned style may not have an effect until {@link #setStyle(Label.LabelStyle)} is
     * called. */
    public Label.LabelStyle getStyle () {
        return style;
    }
    
    /** Sets the text to the specified integer value. If the text is already equivalent to the specified value, a string is not
     * allocated.
     * @return true if the text was changed. */
    public boolean setText (int value) {
        if (this.intValue == value) return false;
        text.clear();
        text.append(value);
        intValue = value;
        invalidateHierarchy();
        return true;
    }
    
    /** @param newText If null, "" will be used. */
    public void setText (@Null CharSequence newText) {
        if (newText == null) {
            if (text.length == 0) return;
            text.clear();
        } else if (newText instanceof StringBuilder) {
            if (text.equals(newText)) return;
            text.clear();
            text.append((StringBuilder)newText);
        } else {
            if (textEquals(newText)) return;
            text.clear();
            text.append(newText);
        }
        intValue = Integer.MIN_VALUE;
        invalidateHierarchy();
    }
    
    public boolean textEquals (CharSequence other) {
        int length = text.length;
        char[] chars = text.chars;
        if (length != other.length()) return false;
        for (int i = 0; i < length; i++)
            if (chars[i] != other.charAt(i)) return false;
        return true;
    }
    
    public StringBuilder getText () {
        return text;
    }
    
    public void invalidate () {
        super.invalidate();
        prefSizeInvalid = true;
    }
    
    private void scaleAndComputePrefSize () {
        BitmapFont font = cache.getFont();
        float oldScaleX = font.getScaleX();
        float oldScaleY = font.getScaleY();
        if (fontScaleChanged) font.getData().setScale(fontScaleX, fontScaleY);
        
        computePrefSize();
        
        if (fontScaleChanged) font.getData().setScale(oldScaleX, oldScaleY);
    }
    
    private void computePrefSize () {
        prefSizeInvalid = false;
        GlyphLayout prefSizeLayout = OpenLabel.prefSizeLayout;
        if (wrap && ellipsis == null) {
            float width = getWidth();
            if (style.background != null) {
                width = Math.max(width, style.background.getMinWidth()) - style.background.getLeftWidth()
                        - style.background.getRightWidth();
            }
            prefSizeLayout.setText(cache.getFont(), text, Color.WHITE, width, Align.left, true);
        } else
            prefSizeLayout.setText(cache.getFont(), text);
        prefSize.set(prefSizeLayout.width, prefSizeLayout.height);
    }
    
    public void layout () {
        BitmapFont font = cache.getFont();
        float oldScaleX = font.getScaleX();
        float oldScaleY = font.getScaleY();
        if (fontScaleChanged) font.getData().setScale(fontScaleX, fontScaleY);
        
        boolean wrap = this.wrap && ellipsis == null;
        if (wrap) {
            float prefHeight = getPrefHeight();
            if (prefHeight != lastPrefHeight) {
                lastPrefHeight = prefHeight;
                invalidateHierarchy();
            }
        }
        
        float width = getWidth(), height = getHeight();
        Drawable background = style.background;
        float x = 0, y = 0;
        if (background != null) {
            x = background.getLeftWidth();
            y = background.getBottomHeight();
            width -= background.getLeftWidth() + background.getRightWidth();
            height -= background.getBottomHeight() + background.getTopHeight();
        }
        
        GlyphLayout layout = this.layout;
        float textWidth, textHeight;
        if (wrap || text.indexOf("\n") != -1) {
            // If the text can span multiple lines, determine the text's actual size so it can be aligned within the label.
            layout.setText(font, text, 0, text.length, Color.WHITE, width, lineAlign, wrap, ellipsis);
            textWidth = layout.width;
            textHeight = layout.height;
            
            if ((labelAlign & Align.left) == 0) {
                if ((labelAlign & Align.right) != 0)
                    x += width - textWidth;
                else
                    x += (width - textWidth) / 2;
            }
        } else {
            textWidth = width;
            textHeight = font.getData().capHeight;
        }
        
        if ((labelAlign & Align.top) != 0) {
            y += cache.getFont().isFlipped() ? 0 : height - textHeight;
            y += style.font.getDescent();
        } else if ((labelAlign & Align.bottom) != 0) {
            y += cache.getFont().isFlipped() ? height - textHeight : 0;
            y -= style.font.getDescent();
        } else {
            y += (height - textHeight) / 2;
        }
        if (!cache.getFont().isFlipped()) y += textHeight;
        
        layout.setText(font, text, 0, text.length, Color.WHITE, textWidth, lineAlign, wrap, ellipsis);
        cache.setText(layout, x, y);
        
        if (fontScaleChanged) font.getData().setScale(oldScaleX, oldScaleY);
    }
    
    public void draw (Batch batch, float parentAlpha) {
        validate();
        Color color = tempColor.set(getColor());
        color.a *= parentAlpha;
        if (style.background != null) {
            batch.setColor(color.r, color.g, color.b, color.a);
            style.background.draw(batch, getX(), getY(), getWidth(), getHeight());
        }
        if (style.fontColor != null) color.mul(style.fontColor);
        cache.tint(color);
        cache.setPosition(getX(), getY());
        cache.draw(batch);
    }
    
    public float getPrefWidth () {
        if (wrap) return 0;
        if (prefSizeInvalid) scaleAndComputePrefSize();
        float width = prefSize.x;
        Drawable background = style.background;
        if (background != null)
            width = Math.max(width + background.getLeftWidth() + background.getRightWidth(), background.getMinWidth());
        return width;
    }
    
    public float getPrefHeight () {
        if (prefSizeInvalid) scaleAndComputePrefSize();
        float descentScaleCorrection = 1;
        if (fontScaleChanged) descentScaleCorrection = fontScaleY / style.font.getScaleY();
        float height = prefSize.y - style.font.getDescent() * descentScaleCorrection * 2;
        Drawable background = style.background;
        if (background != null)
            height = Math.max(height + background.getTopHeight() + background.getBottomHeight(), background.getMinHeight());
        return height;
    }
    
    public GlyphLayout getGlyphLayout () {
        return layout;
    }
    
    /** If false, the text will only wrap where it contains newlines (\n). The preferred size of the label will be the text bounds.
     * If true, the text will word wrap using the width of the label. The preferred width of the label will be 0, it is expected
     * that something external will set the width of the label. Wrapping will not occur when ellipsis is enabled. Default is false.
     * <p>
     * When wrap is enabled, the label's preferred height depends on the width of the label. In some cases the parent of the label
     * will need to layout twice: once to set the width of the label and a second time to adjust to the label's new preferred
     * height. */
    public void setWrap (boolean wrap) {
        this.wrap = wrap;
        invalidateHierarchy();
    }
    
    public boolean getWrap () {
        return wrap;
    }
    
    public int getLabelAlign () {
        return labelAlign;
    }
    
    public int getLineAlign () {
        return lineAlign;
    }
    
    /** @param alignment Aligns all the text within the label (default left center) and each line of text horizontally (default
     *           left).
     * @see Align */
    public void setAlignment (int alignment) {
        setAlignment(alignment, alignment);
    }
    
    /** @param labelAlign Aligns all the text within the label (default left center).
     * @param lineAlign Aligns each line of text horizontally (default left).
     * @see Align */
    public void setAlignment (int labelAlign, int lineAlign) {
        this.labelAlign = labelAlign;
        
        if ((lineAlign & Align.left) != 0)
            this.lineAlign = Align.left;
        else if ((lineAlign & Align.right) != 0)
            this.lineAlign = Align.right;
        else
            this.lineAlign = Align.center;
        
        invalidate();
    }
    
    public void setFontScale (float fontScale) {
        setFontScale(fontScale, fontScale);
    }
    
    public void setFontScale (float fontScaleX, float fontScaleY) {
        fontScaleChanged = true;
        this.fontScaleX = fontScaleX;
        this.fontScaleY = fontScaleY;
        invalidateHierarchy();
    }
    
    public float getFontScaleX () {
        return fontScaleX;
    }
    
    public void setFontScaleX (float fontScaleX) {
        setFontScale(fontScaleX, fontScaleY);
    }
    
    public float getFontScaleY () {
        return fontScaleY;
    }
    
    public void setFontScaleY (float fontScaleY) {
        setFontScale(fontScaleX, fontScaleY);
    }
    
    /** When non-null the text will be truncated "..." if it does not fit within the width of the label. Wrapping will not occur
     * when ellipsis is enabled. Default is false. */
    public void setEllipsis (@Null String ellipsis) {
        this.ellipsis = ellipsis;
    }
    
    /** When true the text will be truncated "..." if it does not fit within the width of the label. Wrapping will not occur when
     * ellipsis is true. Default is false. */
    public void setEllipsis (boolean ellipsis) {
        if (ellipsis)
            this.ellipsis = "...";
        else
            this.ellipsis = null;
    }
    
    /** Allows subclasses to access the cache in {@link #draw(Batch, float)}. */
    protected BitmapFontCache getBitmapFontCache () {
        return cache;
    }
    
    public String toString () {
        String name = getName();
        if (name != null) return name;
        String className = getClass().getName();
        int dotIndex = className.lastIndexOf('.');
        if (dotIndex != -1) className = className.substring(dotIndex + 1);
        return (className.indexOf('$') != -1 ? "Label " : "") + className + ": " + text;
    }
}
